#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
任务统计组件

该组件通过HTTP接口获取客户图案制作进度数据，
并以可视化形式展示整体进度、客户分组网格以及单个客户的详细信息。
"""

from __future__ import annotations

import json
import os
import configparser
from typing import Dict, Any, List, Tuple, Optional
from collections import defaultdict
import asyncio
import threading
import websockets
from urllib.parse import urljoin
import urllib.request

from PyQt6.QtCore import (
    Qt,
    QTimer,
    QDateTime,
    QThread,
    pyqtSignal,
    QSize,
    QThreadPool,
    QRunnable,
    QObject,
)
from PyQt6.QtGui import QFont, QColor, QPixmap, QMouseEvent
from PyQt6.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QFrame,
    QListWidget,
    QListWidgetItem,
    QScrollArea,
    QGridLayout,
    QSizePolicy,
    QSpacerItem,
    QDialog,
    QDialogButtonBox,
    QLineEdit,
)

class ImageViewerDialog(QDialog):
    """图片查看对话框"""
    
    def __init__(self, pixmap: QPixmap, title: str = "图片查看", parent: Optional[QWidget] = None):
        super().__init__(parent)
        self.setWindowTitle(title)
        self.setModal(True)
        self.setMinimumSize(400, 400)
        
        layout = QVBoxLayout(self)
        layout.setContentsMargins(10, 10, 10, 10)
        layout.setSpacing(10)
        
        # 图片显示区域（可滚动）
        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
        
        image_label = QLabel()
        image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        image_label.setScaledContents(False)
        
        # 设置原始图片
        if pixmap and not pixmap.isNull():
            image_label.setPixmap(pixmap)
            # 调整对话框大小以适应图片（但不超过屏幕）
            screen_size = self.screen().availableGeometry().size()
            max_width = min(pixmap.width() + 40, screen_size.width() - 100)
            max_height = min(pixmap.height() + 100, screen_size.height() - 100)
            self.resize(max_width, max_height)
        
        scroll_area.setWidget(image_label)
        layout.addWidget(scroll_area)
        
        # 关闭按钮
        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)


class ImageDownloadSignals(QObject):
    """图片下载信号类，用于跨线程通信"""
    finished = pyqtSignal(str, QPixmap)  # url, pixmap
    error = pyqtSignal(str, str)  # url, error_message


class ImageDownloadTask(QRunnable):
    """图片下载任务，在后台线程执行"""
    
    def __init__(self, url: str, signals: ImageDownloadSignals, timeout: int = 5):
        super().__init__()
        self.url = url
        self.signals = signals
        self.timeout = timeout
    
    def run(self):
        """在后台线程中执行下载"""
        try:
            with urllib.request.urlopen(self.url, timeout=self.timeout) as resp:
                data = resp.read()
            pixmap = QPixmap()
            if pixmap.loadFromData(data):
                self.signals.finished.emit(self.url, pixmap)
            else:
                self.signals.error.emit(self.url, "图片数据格式错误")
        except Exception as e:
            self.signals.error.emit(self.url, str(e))


class PatternItemWidget(QWidget):
    """图案项Widget，支持双击查看图片"""
    
    def __init__(self, record: dict, get_pixmap_func, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self.record = record
        self.get_pixmap_func = get_pixmap_func
        
        layout = QHBoxLayout(self)
        layout.setContentsMargins(8, 6, 8, 6)
        layout.setSpacing(12)

        self.image_label = QLabel()
        self.image_label.setFixedSize(88, 88)
        self.image_label.setObjectName("task_statistics_pattern_thumb")
        self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # 先显示占位符文本
        self.image_label.setText("加载中...")
        layout.addWidget(self.image_label)
        
        # 异步加载图片
        url = record.get("black_box_url", "")
        if url:
            self.get_pixmap_func(url, self._on_image_loaded)

        text_container = QWidget()
        text_layout = QVBoxLayout(text_container)
        text_layout.setContentsMargins(0, 0, 0, 0)
        text_layout.setSpacing(4)

        title = QLabel(f"[{record.get('status', '')}] {record.get('serial_number', '')}")
        title.setObjectName("task_statistics_pattern_title")
        subtitle_parts = []
        if record.get("pdf_name"):
            subtitle_parts.append(record["pdf_name"])
        if record.get("updated_at"):
            subtitle_parts.append(record["updated_at"])
        subtitle = QLabel("  ".join(subtitle_parts) if subtitle_parts else "")
        subtitle.setObjectName("task_statistics_pattern_subtitle")

        text_layout.addWidget(title)
        text_layout.addWidget(subtitle)
        text_layout.addStretch()

        layout.addWidget(text_container, stretch=1)
    
    def _on_image_loaded(self, url: str, pixmap: QPixmap):
        """图片加载完成回调"""
        if pixmap and not pixmap.isNull():
            scaled = pixmap.scaled(88, 88, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
            self.image_label.setPixmap(scaled)
            self.image_label.setText("")  # 清除占位符文本
        else:
            self.image_label.setText("加载失败")
    
    def mouseDoubleClickEvent(self, event: QMouseEvent):
        """双击事件处理"""
        if event.button() == Qt.MouseButton.LeftButton:
            # 异步获取原始图片
            url = self.record.get("black_box_url", "")
            if url:
                def on_large_image_loaded(url: str, pixmap: QPixmap):
                    if pixmap and not pixmap.isNull():
                        title = f"{self.record.get('serial_number', '图片查看')}"
                        dialog = ImageViewerDialog(pixmap, title, self)
                        dialog.exec()
                self.get_pixmap_func(url, on_large_image_loaded)
        super().mouseDoubleClickEvent(event)


class TaskStatisticsCard(QFrame):
    """单个客户卡片，用于在网格中展示客户进度。"""

    clicked = pyqtSignal(str)

    def __init__(self, customer_id: str, parent: QWidget | None = None):
        super().__init__(parent)
        self.customer_id = customer_id
        self.data: Dict[str, Any] = {}

        self.setObjectName("task_statistics_card")
        self.setCursor(Qt.CursorShape.PointingHandCursor)
        self.setFrameShape(QFrame.Shape.StyledPanel)
        self.setFrameShadow(QFrame.Shadow.Raised)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(12, 16, 12, 12)
        layout.setSpacing(8)

        self.id_label = QLabel(customer_id)
        self.id_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.id_label.setObjectName("task_statistics_card_id")
        self.id_label.setFont(QFont("Microsoft YaHei", 18, QFont.Weight.Bold))

        self.info_label = QLabel("--")
        self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.info_label.setObjectName("task_statistics_card_info")
        self.info_label.setFont(QFont("Microsoft YaHei", 12, QFont.Weight.Normal))

        layout.addWidget(self.id_label)
        layout.addWidget(self.info_label)
        layout.addStretch()

    def update_data(self, metrics: Dict[str, Any]):
        """更新卡片显示的信息。"""
        self.data = metrics or {}
        total = metrics.get("total", 0)
        completed = metrics.get("completed", 0)
        in_progress = metrics.get("in_progress", 0)
        failed = metrics.get("failed_qa", 0)

        ratio_text = f"{completed}/{total}" if total else "0/0"
        extra_parts: List[str] = []
        if in_progress:
            extra_parts.append(f"制作中 {in_progress}")
        if failed:
            extra_parts.append(f"质检 {failed}")
        if extra_parts:
            ratio_text += "  " + "  ".join(extra_parts)

        self.info_label.setText(ratio_text)

        status = "pending"
        if total > 0 and completed >= total:
            status = "completed"
        elif in_progress > 0:
            status = "in_progress"
        elif failed > 0:
            status = "failed"

        self.setProperty("status", status)
        self._update_style()

    def set_active(self, active: bool):
        """设置卡片选中状态"""
        self.setProperty("active", active)
        self._update_style()

    def _update_style(self):
        """更新卡片样式"""
        is_active = self.property("active") or False
        
        # 基础样式
        base_style = """
            QFrame#task_statistics_card {
                border: 2px solid #e0e0e0;
                border-radius: 8px;
                background-color: white;
            }
            QFrame#task_statistics_card QLabel#task_statistics_card_id {
                color: #888888;
            }
            QFrame#task_statistics_card QLabel#task_statistics_card_info {
                color: #888888;
            }
        """
        
        # 选中状态的蓝色背景样式，字体颜色为深色（黑色）
        if is_active:
            active_style = """
                QFrame#task_statistics_card {
                    border: 2px solid #2196F3;
                    border-radius: 8px;
                    background-color: #E3F2FD;
                }
                QFrame#task_statistics_card QLabel#task_statistics_card_id {
                    color: #212121;
                }
                QFrame#task_statistics_card QLabel#task_statistics_card_info {
                    color: #212121;
                }
            """
            self.setStyleSheet(active_style)
        else:
            self.setStyleSheet(base_style)
        
        # 刷新样式系统
        self.style().unpolish(self)
        self.style().polish(self)

    def mousePressEvent(self, event):  # type: ignore[override]
        if event.button() == Qt.MouseButton.LeftButton:
            self.clicked.emit(self.customer_id)
        super().mousePressEvent(event)


class OrderStatusWebSocketThread(QThread):
    """后台线程，通过WebSocket订阅订单状态"""

    connected = pyqtSignal()
    disconnected = pyqtSignal()
    error_occurred = pyqtSignal(str)
    snapshot_received = pyqtSignal(list)
    update_received = pyqtSignal(dict)

    def __init__(self, ws_url: str, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self.ws_url = ws_url
        self._loop: Optional[asyncio.AbstractEventLoop] = None
        self._stop_event = threading.Event()
        self._send_queue: Optional[asyncio.Queue] = None
        self._reconnect_delay = 5

    def run(self):

        async def _runner():
            while not self._stop_event.is_set():
                try:
                    await self._connect_once()
                except Exception as exc:
                    if not self._stop_event.is_set():
                        self.error_occurred.emit(str(exc))
                if self._stop_event.is_set():
                    break
                if not self._stop_event.is_set():
                    self.disconnected.emit()
                    # 使用短间隔的sleep，以便快速响应停止信号
                    sleep_interval = 0.5
                    total_slept = 0
                    while total_slept < self._reconnect_delay and not self._stop_event.is_set():
                        await asyncio.sleep(sleep_interval)
                        total_slept += sleep_interval

        self._loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._loop)
        self._send_queue = asyncio.Queue()
        try:
            self._loop.run_until_complete(_runner())
        except RuntimeError as e:
            # 如果事件循环被提前停止，这是正常的
            if "Event loop stopped" not in str(e):
                raise
        finally:
            # 取消所有待处理的任务
            if self._loop and not self._loop.is_closed():
                pending = asyncio.all_tasks(self._loop)
                for task in pending:
                    task.cancel()
                if pending:
                    try:
                        self._loop.run_until_complete(
                            asyncio.gather(*pending, return_exceptions=True)
                        )
                    except Exception:
                        pass
                self._loop.close()
            self._loop = None
            self._send_queue = None

    async def _connect_once(self):
        try:
            async with websockets.connect(self.ws_url, ping_interval=20, ping_timeout=20) as ws:
                self.connected.emit()
                consumer = asyncio.create_task(self._receiver(ws))
                producer = asyncio.create_task(self._sender(ws))
                done, pending = await asyncio.wait(
                    [consumer, producer],
                    return_when=asyncio.FIRST_EXCEPTION
                )
                for task in pending:
                    task.cancel()
                for task in done:
                    if task.exception():
                        raise task.exception()
        except websockets.exceptions.ConnectionClosed as exc:
            self.error_occurred.emit(f"连接关闭: {exc.code} {exc.reason or ''}")
        except Exception:
            raise

    async def _receiver(self, ws):
        async for message in ws:
            try:
                payload = json.loads(message)
            except json.JSONDecodeError:
                continue
            msg_type = payload.get("type")
            if msg_type == "order_status_snapshot":
                data = payload.get("data") or []
                if isinstance(data, list):
                    self.snapshot_received.emit(data)
            elif msg_type == "order_status_update":
                data = payload.get("data") or {}
                if isinstance(data, dict):
                    self.update_received.emit(data)

    async def _sender(self, ws):
        if not self._send_queue:
            return
        while not self._stop_event.is_set():
            message = await self._send_queue.get()
            if message is None:
                break
            try:
                await ws.send(message)
            except Exception as exc:
                self.error_occurred.emit(str(exc))
                break

    def request_snapshot(self):
        self.send_message({"type": "get_status"})

    def send_message(self, payload: dict):
        if not self._loop or not self._send_queue:
            return
        try:
            message = json.dumps(payload, ensure_ascii=False)
            asyncio.run_coroutine_threadsafe(self._send_queue.put(message), self._loop)
        except RuntimeError:
            pass

    def stop(self):
        """停止WebSocket线程"""
        self._stop_event.set()
        if self._loop and not self._loop.is_closed():
            # 发送停止信号到队列
            if self._send_queue:
                try:
                    asyncio.run_coroutine_threadsafe(self._send_queue.put(None), self._loop)
                except RuntimeError:
                    pass
            # 不要直接停止事件循环，让_runner自然退出
            # 如果事件循环正在运行，等待它自然结束
class TaskStatisticsWidget(QWidget):
    """任务统计主组件。"""

    manual_refresh_requested = pyqtSignal()
    data_updated = pyqtSignal()  # 数据更新信号

    def __init__(self, parent: QWidget | None = None):
        super().__init__(parent)
        self.setObjectName("task_statistics_page")

        self.settings_file = "settings.ini"
        # 根据主窗口IP构造URL（初始时使用默认值）
        self._update_urls_from_ip("")
        self.refresh_interval_ms = 15000
        self.customer_cards: Dict[str, TaskStatisticsCard] = {}
        self.order_status_records: Dict[str, Dict[str, Any]] = {}
        self.customer_groups: Dict[str, Dict[str, Any]] = {}
        self.last_fetch_result: Dict[str, Any] = {}
        self._ws_connected = False
        self.ws_thread: Optional[OrderStatusWebSocketThread] = None
        self._current_customer_id: Optional[str] = None
        self._pixmap_cache: Dict[str, QPixmap] = {}
        self._pixmap_downloading: Dict[str, List] = {}  # url -> [callbacks]
        self._thread_pool = QThreadPool()
        self._thread_pool.setMaxThreadCount(5)  # 最多5个并发下载

        self._init_ui()
        self._setup_timer()
        self._init_ws_thread()
        self.fetch_data()
        self.destroyed.connect(self._on_destroyed)

    # --------------------------- UI 构建 --------------------------- #

    def _init_ui(self):
        main_layout = QHBoxLayout(self)
        main_layout.setContentsMargins(10, 10, 10, 10)
        main_layout.setSpacing(12)

        # 左侧：概览 + 客户网格
        self.overview_container = QWidget()
        self.overview_container.setObjectName("task_statistics_overview")
        overview_layout = QVBoxLayout(self.overview_container)
        overview_layout.setContentsMargins(0, 0, 0, 0)
        overview_layout.setSpacing(12)

        overview_layout.addWidget(self._build_header_section())
        overview_layout.addWidget(self._build_summary_section())
        overview_layout.addWidget(self._build_customer_grid_section())

        # 右侧：详情面板
        self.detail_container = QWidget()
        self.detail_container.setObjectName("task_statistics_detail_container")
        detail_layout = QVBoxLayout(self.detail_container)
        detail_layout.setContentsMargins(0, 0, 0, 0)
        detail_layout.setSpacing(10)

        self.detail_title = QLabel("客户详情")
        self.detail_title.setObjectName("task_statistics_detail_title")
        self.detail_title.setFont(QFont("Microsoft YaHei", 13, QFont.Weight.Bold))

        self.detail_progress_label = QLabel("")
        self.detail_progress_label.setObjectName("task_statistics_detail_progress")

        self.detail_stats_labels: Dict[str, QLabel] = {}
        stats_frame = QFrame()
        stats_frame.setObjectName("task_statistics_detail_stats")
        stats_layout = QGridLayout(stats_frame)
        stats_layout.setContentsMargins(12, 8, 12, 8)
        stats_layout.setSpacing(8)

        for idx, (key, text) in enumerate(
            [
                ("total", "总图案"),
                ("completed", "制作好"),
                ("in_progress", "制作中"),
                ("not_started", "没制作"),
                ("failed_qa", "不合格"),
            ]
        ):
            label_title = QLabel(text)
            label_title.setObjectName("task_statistics_detail_stats_title")
            value_label = QLabel("0")
            value_label.setObjectName("task_statistics_detail_stats_value")
            self.detail_stats_labels[key] = value_label
            stats_layout.addWidget(label_title, idx // 2, (idx % 2) * 2)
            stats_layout.addWidget(value_label, idx // 2, (idx % 2) * 2 + 1)

        self.detail_note_label = QLabel("")
        self.detail_note_label.setObjectName("task_statistics_detail_note")
        self.detail_note_label.setWordWrap(True)

        self.pattern_list = QListWidget()
        self.pattern_list.setObjectName("task_statistics_pattern_list")

        self.detail_placeholder = QLabel("请选择左侧客户卡片查看详细信息")
        self.detail_placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.detail_placeholder.setObjectName("task_statistics_detail_placeholder")

        detail_layout.addWidget(self.detail_title)
        detail_layout.addWidget(self.detail_progress_label)
        detail_layout.addWidget(stats_frame)
        detail_layout.addWidget(self.detail_note_label)
        detail_layout.addWidget(self.pattern_list, stretch=1)
        detail_layout.addWidget(self.detail_placeholder)

        self._show_placeholder(True)

        main_layout.addWidget(self.overview_container, stretch=2)
        main_layout.addWidget(self.detail_container, stretch=1)

    def _init_ws_thread(self):
        self._stop_ws_thread()
        if not self.recognition_ws_url:
            print("⚠️ 未配置任务统计WebSocket地址")
            return
        self.ws_thread = OrderStatusWebSocketThread(self.recognition_ws_url, self)
        self.ws_thread.connected.connect(self._on_ws_connected)
        self.ws_thread.disconnected.connect(self._on_ws_disconnected)
        self.ws_thread.error_occurred.connect(self._on_ws_error)
        self.ws_thread.snapshot_received.connect(self._on_ws_snapshot)
        self.ws_thread.update_received.connect(self._on_ws_update)
        self.ws_thread.start()

    def _stop_ws_thread(self):
        if self.ws_thread:
            self.ws_thread.stop()
            self.ws_thread.wait(2000)
            self.ws_thread = None

    def _restart_ws_thread(self):
        self._init_ws_thread()
        # 重新连接后，通知主窗口更新状态
        self._notify_main_window_status()
    
    def _notify_main_window_status(self):
        """通知主窗口更新任务服务器状态"""
        main_window = self._get_main_window()
        if main_window and hasattr(main_window, 'update_task_server_status'):
            if self._ws_connected:
                main_window.update_task_server_status("已连接")
            else:
                main_window.update_task_server_status("未连接")

    def _on_destroyed(self):
        self._stop_ws_thread()

    def _build_header_section(self) -> QWidget:
        header = QWidget()
        layout = QHBoxLayout(header)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(8)

        self.title_label = QLabel("客户图案制作进度")
        self.title_label.setObjectName("task_statistics_title")
        self.title_label.setFont(QFont("Microsoft YaHei", 16, QFont.Weight.Bold))
        self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)

        layout.addWidget(self.title_label)
        layout.addStretch()

        return header

    def _build_summary_section(self) -> QWidget:
        frame = QFrame()
        frame.setObjectName("task_statistics_summary_frame")
        layout = QGridLayout(frame)
        layout.setContentsMargins(12, 12, 12, 12)
        layout.setSpacing(10)

        self.summary_labels: Dict[str, QLabel] = {}

        summary_items = [
            ("progress", "总进度"),
            ("not_started", "没制作"),
            ("in_progress", "制作中"),
            ("completed", "制作好"),
            ("failed_qa", "不合格"),
        ]

        for idx, (key, text) in enumerate(summary_items):
            title = QLabel(text)
            title.setObjectName("task_statistics_summary_title")
            value = QLabel("--")
            value.setObjectName("task_statistics_summary_value")
            self.summary_labels[key] = value

            layout.addWidget(title, idx // 3, (idx % 3) * 2)
            layout.addWidget(value, idx // 3, (idx % 3) * 2 + 1)

        return frame

    def _build_customer_grid_section(self) -> QWidget:
        container = QWidget()
        container.setObjectName("task_statistics_grid_container")
        layout = QVBoxLayout(container)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(8)

        self.grid_scroll = QScrollArea()
        self.grid_scroll.setWidgetResizable(True)
        self.grid_scroll.setObjectName("task_statistics_grid_scroll")

        self.grid_widget = QWidget()
        self.grid_layout = QGridLayout(self.grid_widget)
        self.grid_layout.setContentsMargins(4, 4, 4, 4)
        self.grid_layout.setHorizontalSpacing(10)
        self.grid_layout.setVerticalSpacing(10)

        self.grid_scroll.setWidget(self.grid_widget)
        layout.addWidget(self.grid_scroll)

        return container

    # --------------------------- 数据处理 --------------------------- #

    def _setup_timer(self):
        self.refresh_timer = QTimer(self)
        self.refresh_timer.setInterval(self.refresh_interval_ms)
        self.refresh_timer.timeout.connect(self.fetch_data)
        self.refresh_timer.start()

    def fetch_data(self):
        """请求最新任务统计数据。"""
        if self.ws_thread and self.ws_thread.isRunning():
            if self._ws_connected:
                self.ws_thread.request_snapshot()
            else:
                print("⚠️ 正在等待任务统计服务连接...")
        else:
            print("⚠️ 任务统计WebSocket未启动")

    def _on_ws_connected(self):
        self._ws_connected = True
        print("✅ 已连接任务统计服务")
        if self.ws_thread:
            self.ws_thread.request_snapshot()
        # 通知主窗口更新状态
        self._notify_main_window_status()

    def _on_ws_disconnected(self):
        self._ws_connected = False
        print("⚠️ 任务统计服务断开，正在尝试重连...")
        # 通知主窗口更新状态
        self._notify_main_window_status()

    def _on_ws_error(self, message: str):
        print(f"⚠️ 任务统计服务错误: {message}")
        # 通知主窗口更新状态
        main_window = self._get_main_window()
        if main_window and hasattr(main_window, 'update_task_server_status'):
            main_window.update_task_server_status("连接失败")

    def _on_ws_snapshot(self, records: List[dict]):
        self._apply_snapshot(records)

    def _on_ws_update(self, record: dict):
        self._apply_update(record)

    def _apply_snapshot(self, records: List[dict]):
        updated: Dict[str, Dict[str, Any]] = {}
        if isinstance(records, list):
            for record in records:
                normalized = self._normalize_record(record)
                if normalized:
                    updated[normalized["serial_number"]] = normalized
        self.order_status_records = updated
        self._refresh_view()
        self.data_updated.emit()

    def _apply_update(self, record: dict):
        normalized = self._normalize_record(record)
        if not normalized:
            return
        self.order_status_records[normalized["serial_number"]] = normalized
        self._refresh_view(prefer_customer=normalized["identifier"])
        self.data_updated.emit()

    def _normalize_record(self, record: dict) -> Optional[dict]:
        if not isinstance(record, dict):
            return None
        serial = str(record.get("serial_number") or "").strip()
        if not serial:
            return None
        identifier = str(record.get("identifier") or "").strip() or "未识别"
        status = str(record.get("status") or "未制作").strip()
        pdf_name = str(record.get("pdf_name") or "").strip()
        updated_at = str(record.get("updated_at") or "").strip()
        black_box_path = str(record.get("black_box_path") or "").strip()
        black_box_url = str(record.get("black_box_url") or "").strip()
        if not black_box_url and black_box_path:
            normalized_path = black_box_path.replace("\\", "/").lstrip("/")
            if self.recognition_file_base_url:
                black_box_url = f"{self.recognition_file_base_url}/{normalized_path}"
            else:
                black_box_url = normalized_path
        return {
            "serial_number": serial,
            "identifier": identifier,
            "status": status,
            "pdf_name": pdf_name,
            "updated_at": updated_at,
            "black_box_path": black_box_path,
            "black_box_url": black_box_url,
        }

    def _create_pattern_item_widget(self, record: dict) -> QWidget:
        """创建图案项Widget，支持双击查看图片"""
        widget = PatternItemWidget(record, self._get_pixmap, self)
        return widget

    def _get_pixmap(self, url: str, callback=None):
        """
        异步获取图片
        
        Args:
            url: 图片URL
            callback: 回调函数 callback(url: str, pixmap: QPixmap)，如果为None则同步返回（兼容旧代码）
        
        Returns:
            如果callback为None，返回QPixmap或None（同步模式，已废弃）
            如果callback不为None，返回None（异步模式）
        """
        if not url:
            if callback:
                callback(url, None)
            return None
        
        # 如果缓存中有，立即返回
        if url in self._pixmap_cache:
            pixmap = self._pixmap_cache[url]
            if callback:
                callback(url, pixmap)
            else:
                return pixmap
        
        # 如果正在下载中，添加回调到队列
        if url in self._pixmap_downloading:
            if callback:
                self._pixmap_downloading[url].append(callback)
            return None
        
        # 如果没有callback，使用同步模式（兼容旧代码，但不推荐）
        if callback is None:
            try:
                print(f"[TaskStatistics] 同步下载黑框图像: {url}")
                with urllib.request.urlopen(url, timeout=5) as resp:
                    data = resp.read()
                pixmap = QPixmap()
                if pixmap.loadFromData(data):
                    self._pixmap_cache[url] = pixmap
                    return pixmap
            except Exception:
                return None
            return None
        
        # 异步下载模式
        print(f"[TaskStatistics] 异步下载黑框图像: {url}")
        self._pixmap_downloading[url] = [callback]
        
        # 创建信号对象
        signals = ImageDownloadSignals()
        signals.finished.connect(self._on_pixmap_downloaded)
        signals.error.connect(self._on_pixmap_download_error)
        
        # 创建下载任务并提交到线程池
        task = ImageDownloadTask(url, signals)
        self._thread_pool.start(task)
    
    def _on_pixmap_downloaded(self, url: str, pixmap: QPixmap):
        """图片下载完成回调"""
        # 保存到缓存
        if pixmap and not pixmap.isNull():
            self._pixmap_cache[url] = pixmap
        
        # 调用所有等待的回调
        callbacks = self._pixmap_downloading.pop(url, [])
        for callback in callbacks:
            try:
                callback(url, pixmap)
            except Exception as e:
                print(f"[TaskStatistics] 回调执行错误: {e}")
    
    def _on_pixmap_download_error(self, url: str, error_msg: str):
        """图片下载错误回调"""
        print(f"[TaskStatistics] 下载图片失败: {url}, 错误: {error_msg}")
        
        # 调用所有等待的回调，传递None表示失败
        callbacks = self._pixmap_downloading.pop(url, [])
        for callback in callbacks:
            try:
                callback(url, None)
            except Exception as e:
                print(f"[TaskStatistics] 回调执行错误: {e}")

    def _refresh_view(self, prefer_customer: Optional[str] = None):
        self._recalculate_groups()
        self._update_summary()
        self._update_customer_grid()
        self._restore_selection(prefer_customer)

    def _recalculate_groups(self):
        groups: Dict[str, Dict[str, Any]] = {}
        for record in self.order_status_records.values():
            identifier = record["identifier"]
            group = groups.setdefault(identifier, {"identifier": identifier, "items": []})
            group["items"].append(record)
        for group in groups.values():
            group["metrics"] = self._build_group_metrics(group["items"])
        self.customer_groups = groups

    def _classify_status(self, status: str) -> str:
        normalized = (status or "").strip()
        if normalized in {"未制作", "未开始", "未处理", "待制作"}:
            return "not_started"
        if normalized in {"制作中"}:
            return "in_progress"
        if normalized in {"制作完成", "制作好", "完成", "已完成"}:
            return "completed"
        if "质检" in normalized:
            return "failed_qa"
        return "other"

    def _build_group_metrics(self, items: List[dict]) -> Dict[str, int]:
        metrics = {
            "total": len(items),
            "not_started": 0,
            "in_progress": 0,
            "completed": 0,
            "failed_qa": 0,
        }
        for item in items:
            category = self._classify_status(item.get("status", ""))
            if category in metrics:
                metrics[category] += 1
        return metrics

    def _update_summary(self):
        totals = {"not_started": 0, "in_progress": 0, "completed": 0, "failed_qa": 0}
        total = len(self.order_status_records)
        for record in self.order_status_records.values():
            category = self._classify_status(record.get("status", ""))
            if category in totals:
                totals[category] += 1
        completed = totals["completed"]
        progress_text = "--"
        if total > 0:
            progress_ratio = completed / total * 100
            progress_text = f"{progress_ratio:.0f}% ({completed}/{total})"
        self.summary_labels["progress"].setText(progress_text)
        self.summary_labels["not_started"].setText(str(totals["not_started"]))
        self.summary_labels["in_progress"].setText(str(totals["in_progress"]))
        self.summary_labels["completed"].setText(str(totals["completed"]))
        self.summary_labels["failed_qa"].setText(str(totals["failed_qa"]))

    def _update_customer_grid(self):
        # 先收集需要保留的卡片ID
        current_ids = set(self.customer_groups.keys())
        
        # 清空布局并删除所有非卡片widget（如header_label）
        # 卡片widget会从布局中移除但保留引用以便重用
        while self.grid_layout.count():
            item = self.grid_layout.takeAt(0)
            if item.widget():
                widget = item.widget()
                # 只删除非卡片widget（如header_label），卡片widget保留以便重用
                if not isinstance(widget, TaskStatisticsCard):
                    widget.deleteLater()
        
        # 删除不再需要的卡片
        cards_to_delete = []
        for card_id in list(self.customer_cards.keys()):
            if card_id not in current_ids:
                card = self.customer_cards.pop(card_id)
                if card:
                    cards_to_delete.append(card)
        
        # 安全删除卡片
        for card in cards_to_delete:
            try:
                # 检查卡片是否还存在（通过访问parent属性）
                _ = card.parent()
                card.deleteLater()
            except RuntimeError:
                # 卡片已被删除，忽略
                pass

        grouped_by_prefix: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
        for identifier, data in sorted(self.customer_groups.items()):
            prefix = identifier[0] if identifier and identifier[0].isdigit() else "其他"
            grouped_by_prefix[prefix].append(data)

        row = 0
        for prefix in sorted(grouped_by_prefix.keys()):
            header_label = QLabel(f"{prefix}开头客户")
            header_label.setObjectName("task_statistics_group_header")
            header_label.setFont(QFont("Microsoft YaHei", 12, QFont.Weight.Bold))
            self.grid_layout.addWidget(header_label, row, 0, 1, 6)
            row += 1

            col = 0
            for group in grouped_by_prefix[prefix]:
                identifier = group["identifier"]
                metrics = group["metrics"]
                card = self.customer_cards.get(identifier)
                
                # 检查卡片是否存在且有效
                if not card:
                    card = TaskStatisticsCard(identifier)
                    card.clicked.connect(self._on_card_clicked)
                    self.customer_cards[identifier] = card
                else:
                    # 检查卡片是否已被删除
                    try:
                        _ = card.parent()
                    except RuntimeError:
                        # 卡片已被删除，重新创建
                        card = TaskStatisticsCard(identifier)
                        card.clicked.connect(self._on_card_clicked)
                        self.customer_cards[identifier] = card
                
                card.update_data(metrics)
                self.grid_layout.addWidget(card, row, col)
                col += 1
                if col >= 6:
                    col = 0
                    row += 1
            if col != 0:
                row += 1

        self.grid_layout.setRowStretch(row, 1)
        
        # 设置选中卡片的背景色和边框（如果有当前选中的客户）
        if self._current_customer_id:
            for card_id, card in self.customer_cards.items():
                is_active = card_id == self._current_customer_id
                card.set_active(is_active)

    def _show_customer_details(self, customer_id: str):
        group = self.customer_groups.get(customer_id)
        if not group:
            self._show_placeholder(True)
            self._current_customer_id = None
            return

        metrics = group["metrics"]
        items = sorted(group["items"], key=lambda x: x["serial_number"])

        total = metrics.get("total", 0)
        completed = metrics.get("completed", 0)
        progress_text = f"进度: {completed}/{total}" if total else "进度: --"
        self.detail_title.setText(f"客户 {customer_id}")
        self.detail_progress_label.setText(progress_text)

        for key, label in self.detail_stats_labels.items():
            label.setText(str(metrics.get(key, 0)))

        pdf_list = sorted({item["pdf_name"] for item in items if item.get("pdf_name")})
        if pdf_list:
            self.detail_note_label.setText("关联PDF: " + "、".join(pdf_list))
            self.detail_note_label.show()
        else:
            self.detail_note_label.hide()

        self.pattern_list.clear()
        for record in items:
            item_widget = self._create_pattern_item_widget(record)
            list_item = QListWidgetItem()
            list_item.setSizeHint(item_widget.sizeHint())
            self.pattern_list.addItem(list_item)
            self.pattern_list.setItemWidget(list_item, item_widget)
        self.pattern_list.scrollToTop()
        self._show_placeholder(False)

        # 设置选中卡片的背景色和边框
        for card_id, card in self.customer_cards.items():
            card.set_active(card_id == customer_id)

        self._current_customer_id = customer_id

    def _status_color(self, status: str) -> Optional[str]:
        normalized = (status or "").strip()
        if normalized in {"制作完成", "制作好", "完成", "已完成"}:
            return "#28a745"
        if normalized in {"制作中"}:
            return "#ff9800"
        if "质检" in normalized:
            return "#dc3545"
        if normalized in {"未制作", "未开始", "未处理", "待制作"}:
            return "#6c757d"
        return "#1976d2"

    def _restore_selection(self, prefer_customer: Optional[str] = None):
        if not self.customer_groups:
            self._current_customer_id = None
            self._show_placeholder(True)
            return
        candidate = prefer_customer or self._current_customer_id
        if not candidate or candidate not in self.customer_groups:
            candidate = sorted(self.customer_groups.keys())[0]
        self._current_customer_id = candidate
        self._show_customer_details(candidate)

    def _show_placeholder(self, visible: bool):
        self.detail_placeholder.setVisible(visible)
        self.detail_title.setVisible(not visible)
        self.detail_progress_label.setVisible(not visible)
        if visible:
            self.detail_note_label.hide()
        else:
            self.detail_note_label.setVisible(bool(self.detail_note_label.text()))
        self.pattern_list.setVisible(not visible)

    # --------------------------- 设置与配置 --------------------------- #

    def _update_urls_from_ip(self, ip: str):
        """根据IP地址更新所有URL"""
        if not ip or not ip.strip():
            # 使用默认值
            ip = "127.0.0.1"
        else:
            ip = ip.strip()
        
        # 构造各个URL
        self.api_base_url = f"http://{ip}:80"
        self.recognition_ws_url = f"ws://{ip}:8766"
        self.recognition_file_base_url = f"http://{ip}:8767/files"
        
        print(f"📝 任务统计URL已更新: IP={ip}")
        print(f"   API: {self.api_base_url}")
        print(f"   WebSocket: {self.recognition_ws_url}")
        print(f"   文件服务: {self.recognition_file_base_url}")
    
    def _update_ws_url_from_config(self):
        """从主窗口获取IP并更新URL（供外部调用）"""
        # 尝试从主窗口获取IP
        main_window = self._get_main_window()
        if main_window and hasattr(main_window, 'server_ip_input'):
            ip = main_window.server_ip_input.text().strip()
            old_ws_url = self.recognition_ws_url
            self._update_urls_from_ip(ip)
            # 如果WebSocket URL发生变化，重新连接
            if self.recognition_ws_url != old_ws_url:
                print(f"🔄 WebSocket URL变化，重新连接: {old_ws_url} -> {self.recognition_ws_url}")
                self._restart_ws_thread()
        else:
            # 尝试从配置文件读取IP
            try:
                config = configparser.ConfigParser()
                if os.path.exists(self.settings_file):
                    config.read(self.settings_file, encoding="utf-8")
                    if config.has_option('Settings', 'server_ip'):
                        ip = config.get('Settings', 'server_ip').strip()
                        if ip:
                            old_ws_url = self.recognition_ws_url
                            self._update_urls_from_ip(ip)
                            if self.recognition_ws_url != old_ws_url:
                                print(f"🔄 WebSocket URL变化，重新连接: {old_ws_url} -> {self.recognition_ws_url}")
                                self._restart_ws_thread()
            except Exception as e:
                print(f"⚠️ 获取IP失败: {e}")
    
    def _get_main_window(self):
        """获取主窗口引用"""
        parent = self.parent()
        while parent:
            if hasattr(parent, 'server_ip_input'):
                return parent
            parent = parent.parent()
        return None

    # --------------------------- 外部接口 --------------------------- #

    def trigger_manual_refresh(self):
        """供外部调用的手动刷新接口。"""
        self.fetch_data()
    
    def get_all_records(self) -> List[Dict[str, Any]]:
        """获取所有订单状态记录"""
        return list(self.order_status_records.values())
    
    def update_order_status(self, serial_number: str, status: str):
        """更新订单状态
        
        Returns:
            bool: 如果成功发送或加入队列返回True，否则返回False
        """
        if not self.ws_thread:
            print(f"⚠️ WebSocket线程未初始化，无法更新状态: {serial_number} -> {status}")
            return False
        
        # 检查连接状态（同时检查内部状态和主窗口显示的状态，确保一致性）
        main_window = self._get_main_window()
        main_window_shows_connected = False
        if main_window and hasattr(main_window, '_task_server_status'):
            main_window_shows_connected = main_window._task_server_status == "已连接"
        
        if not self._ws_connected:
            # 如果内部状态显示未连接，但主窗口显示已连接，说明状态不同步
            if main_window_shows_connected:
                print(f"⚠️ 状态不一致：主窗口显示已连接，但内部状态显示未连接。尝试发送更新: {serial_number} -> {status}")
                # 仍然尝试发送，因为可能只是状态同步延迟
            else:
                print(f"⚠️ WebSocket未连接，无法更新状态: {serial_number} -> {status}")
                return False
        
        try:
            self.ws_thread.send_message({
                "type": "update_status",
                "serial_number": serial_number,
                "status": status
            })
            print(f"✅ 已发送状态更新请求: {serial_number} -> {status}")
            return True
        except Exception as e:
            print(f"❌ 发送状态更新请求失败: {serial_number} -> {status}, 错误: {e}")
            return False

    # --------------------------- 设置与配置 --------------------------- #

    def _on_card_clicked(self, identifier: str):
        if identifier:
            self._show_customer_details(identifier)

